🎖️GitЯра🎖️
Node / meshtastic / Meshtastic-Android / files / core / service / src / jvmMain / kotlin / org / meshtastic / core / service / SecurityKeyBackupStoreImpl.kt
Displaying Raw • Download
core/service/src/jvmMain/kotlin/org/meshtastic/core/service/SecurityKeyBackupStoreImpl.kt 4d4070c8e1ed73a56ee2f054300a0f62e3fe2e62 (4d4070c8) Text, 6.39 KB
T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.core.service
Tff7b72import T7ee787co.touchlab.kermit.Logger
Tff7b72import T7ee787org.koin.core.annotation.Single
Tff7b72import T7ee787org.meshtastic.core.database.desktopDataDir
Tff7b72import T7ee787org.meshtastic.core.repository.SecurityKeyBackupStore
Tff7b72import T7ee787org.meshtastic.core.repository.StoredSecurityKeys
Tff7b72import T7ee787java.io.File
Tff7b72import T7ee787java.io.FileInputStream
Tff7b72import T7ee787java.io.FileOutputStream
Tff7b72import T7ee787java.security.KeyStore
Tff7b72import T7ee787javax.crypto.Cipher
Tff7b72import T7ee787javax.crypto.KeyGenerator
Tff7b72import T7ee787javax.crypto.SecretKey
Tff7b72import T7ee787javax.crypto.spec.GCMParameterSpec
T8b949e/**
* File-backed encrypted key-backup store for JVM/Desktop, mirroring [LockdownPassphraseStoreImpl]'s desktop
* counterpart. Uses a PKCS12 KeyStore to hold an AES-256 master key and AES-256-GCM to encrypt each node's key backup,
* stored as individual `.enc` files under `$MESHTASTIC_DATA_DIR/security_keys/`.
*/
Tf0883e@SingleTb4b4b4(Te6edf3binds Tff7b72= Tff7b72[Te6edf3SecurityKeyBackupStoreTff7b72::Te6edf3classTff7b72]Tb4b4b4)
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4)
Tff7b72class T56d364SecurityKeyBackupStoreImpl Tb4b4b4: Te6edf3SecurityKeyBackupStore Tb4b4b4{
Tff7b72private Tff7b72val Te6edf3storeDirTb4b4b4: Te6edf3File Tff7b72by Te6edf3lazy Tb4b4b4{ Te6edf3FileTb4b4b4(Te6edf3desktopDataDirTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3STORE_DIRTb4b4b4)Tb4b4b4.Te6edf3also Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3mkdirsTb4b4b4(Tb4b4b4) Tb4b4b4} Tb4b4b4}
Tff7b72private Tff7b72val Te6edf3masterKeyTb4b4b4: Te6edf3SecretKey? Tff7b72by Te6edf3lazy Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3loadOrCreateMasterKeyTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3eTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffSecurityKeyBackup: Failed to initialize desktop keystoreTa5d6ff" Tb4b4b4}
Tff7b72null
Tb4b4b4}
Tb4b4b4}
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffReturnCountTa5d6ff"Tb4b4b4)
Tff7b72override Tff7b72fun Td2a8ffgetTb4b4b4(Te6edf3nodeNumTb4b4b4: Tffa657IntTb4b4b4)Tb4b4b4: Te6edf3StoredSecurityKeys? Tb4b4b4{
Tff7b72val Te6edf3key Tff7b72= Te6edf3masterKey Tff7b72?: Tff7b72return Tff7b72null
Tff7b72val Te6edf3file Tff7b72= Te6edf3entryFileTb4b4b4(Te6edf3nodeNumTb4b4b4)
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3fileTb4b4b4.Te6edf3existsTb4b4b4(Tb4b4b4)Tb4b4b4) Tff7b72return Tff7b72null
Tff7b72return Tff7b72try Tb4b4b4{
Tff7b72val Te6edf3plaintext Tff7b72= Te6edf3decryptTb4b4b4(Te6edf3keyTb4b4b4, Te6edf3fileTb4b4b4.Te6edf3readBytesTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3deserializeTb4b4b4(Te6edf3plaintextTb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3eTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffSecurityKeyBackup: Failed to read key backup for nodeTa5d6ff" Tb4b4b4}
Tff7b72null
Tb4b4b4}
Tb4b4b4}
Tff7b72override Tff7b72fun Td2a8ffsaveTb4b4b4(Te6edf3nodeNumTb4b4b4: Tffa657IntTb4b4b4, Te6edf3publicKeyBase64Tb4b4b4: Tffa657StringTb4b4b4, Te6edf3privateKeyBase64Tb4b4b4: Tffa657StringTb4b4b4, Te6edf3timestampTb4b4b4: Tffa657LongTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3key Tff7b72= Te6edf3masterKey Tff7b72?: Te6edf3errorTb4b4b4(Ta5d6ff"Ta5d6ffSecurityKeyBackup: Cannot save keys - keystore unavailableTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3plaintext Tff7b72= Ta5d6ff"Tffd700$Te6edf3timestampTa5d6ff\Ta5d6ffnTffd700$Te6edf3publicKeyBase64Ta5d6ff\Ta5d6ffnTffd700$Te6edf3privateKeyBase64Ta5d6ff"Tb4b4b4.Te6edf3encodeToByteArrayTb4b4b4(Tb4b4b4)
Te6edf3entryFileTb4b4b4(Te6edf3nodeNumTb4b4b4)Tb4b4b4.Te6edf3writeBytesTb4b4b4(Te6edf3encryptTb4b4b4(Te6edf3keyTb4b4b4, Te6edf3plaintextTb4b4b4)Tb4b4b4)
Tb4b4b4}
Tff7b72override Tff7b72fun Td2a8ffdeleteTb4b4b4(Te6edf3nodeNumTb4b4b4: Tffa657IntTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3file Tff7b72= Te6edf3entryFileTb4b4b4(Te6edf3nodeNumTb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3fileTb4b4b4.Te6edf3existsTb4b4b4(Tb4b4b4) Tff7b72&Tff7b72& Tff7b72!Te6edf3fileTb4b4b4.Te6edf3deleteTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{ Ta5d6ff"Ta5d6ffSecurityKeyBackup: Key backup file was not deleted for nodeTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tff7b72private Tff7b72fun Td2a8ffentryFileTb4b4b4(Te6edf3nodeNumTb4b4b4: Tffa657IntTb4b4b4)Tb4b4b4: Te6edf3File Tff7b72= Te6edf3FileTb4b4b4(Te6edf3storeDirTb4b4b4, Ta5d6ff"Tffd700$Te6edf3nodeNumTa5d6ff.encTa5d6ff"Tb4b4b4)
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffReturnCountTa5d6ff"Tb4b4b4)
Tff7b72private Tff7b72fun Td2a8ffdeserializeTb4b4b4(Te6edf3plaintextTb4b4b4: Te6edf3ByteArrayTb4b4b4)Tb4b4b4: Te6edf3StoredSecurityKeys? Tb4b4b4{
Tff7b72val Te6edf3parts Tff7b72= Te6edf3plaintextTb4b4b4.Te6edf3decodeToStringTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3splitTb4b4b4(Ta5d6ff"Ta5d6ff\Ta5d6ffnTa5d6ff"Tb4b4b4, Te6edf3limit Tff7b72= T79c0ff3Tb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3partsTb4b4b4.Te6edf3size Tff7b72!Tff7b72= Te6edf3SERIALIZED_LINE_COUNTTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{ Ta5d6ff"Ta5d6ffSecurityKeyBackup: Invalid key backup entry formatTa5d6ff" Tb4b4b4}
Tff7b72return Tff7b72null
Tb4b4b4}
Tff7b72val Te6edf3timestamp Tff7b72= Te6edf3partsTff7b72[T79c0ff0Tff7b72]Tb4b4b4.Te6edf3toLongOrNullTb4b4b4(Tb4b4b4) Tff7b72?: Tff7b72return Tff7b72null
Tff7b72return Te6edf3StoredSecurityKeysTb4b4b4(Te6edf3publicKeyBase64 Tff7b72= Te6edf3partsTff7b72[T79c0ff1Tff7b72]Tb4b4b4, Te6edf3privateKeyBase64 Tff7b72= Te6edf3partsTff7b72[T79c0ff2Tff7b72]Tb4b4b4, Te6edf3timestamp Tff7b72= Te6edf3timestampTb4b4b4)
Tb4b4b4}
T8b949e// region Encryption
Tff7b72private Tff7b72fun Td2a8ffencryptTb4b4b4(Te6edf3keyTb4b4b4: Te6edf3SecretKeyTb4b4b4, Te6edf3plaintextTb4b4b4: Te6edf3ByteArrayTb4b4b4)Tb4b4b4: Te6edf3ByteArray Tb4b4b4{
Tff7b72val Te6edf3cipher Tff7b72= Te6edf3CipherTb4b4b4.Te6edf3getInstanceTb4b4b4(Te6edf3AES_GCM_TRANSFORMTb4b4b4)
Te6edf3cipherTb4b4b4.Te6edf3initTb4b4b4(Te6edf3CipherTb4b4b4.Te6edf3ENCRYPT_MODETb4b4b4, Te6edf3keyTb4b4b4)
Tff7b72val Te6edf3iv Tff7b72= Te6edf3cipherTb4b4b4.Te6edf3iv
Tff7b72val Te6edf3ciphertext Tff7b72= Te6edf3cipherTb4b4b4.Te6edf3doFinalTb4b4b4(Te6edf3plaintextTb4b4b4)
T8b949e// Format: [1 byte IV length][IV][ciphertext]
Tff7b72return Te6edf3byteArrayOfTb4b4b4(Te6edf3ivTb4b4b4.Te6edf3sizeTb4b4b4.Te6edf3toByteTb4b4b4(Tb4b4b4)Tb4b4b4) Tff7b72+ Te6edf3iv Tff7b72+ Te6edf3ciphertext
Tb4b4b4}
Tff7b72private Tff7b72fun Td2a8ffdecryptTb4b4b4(Te6edf3keyTb4b4b4: Te6edf3SecretKeyTb4b4b4, Te6edf3dataTb4b4b4: Te6edf3ByteArrayTb4b4b4)Tb4b4b4: Te6edf3ByteArray Tb4b4b4{
Tff7b72val Te6edf3ivLength Tff7b72= Te6edf3dataTff7b72[T79c0ff0Tff7b72]Tb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4) Te6edf3and Te6edf3BYTE_MASK
Tff7b72val Te6edf3iv Tff7b72= Te6edf3dataTb4b4b4.Te6edf3copyOfRangeTb4b4b4(T79c0ff1Tb4b4b4, T79c0ff1 Tff7b72+ Te6edf3ivLengthTb4b4b4)
Tff7b72val Te6edf3ciphertext Tff7b72= Te6edf3dataTb4b4b4.Te6edf3copyOfRangeTb4b4b4(T79c0ff1 Tff7b72+ Te6edf3ivLengthTb4b4b4, Te6edf3dataTb4b4b4.Te6edf3sizeTb4b4b4)
Tff7b72val Te6edf3cipher Tff7b72= Te6edf3CipherTb4b4b4.Te6edf3getInstanceTb4b4b4(Te6edf3AES_GCM_TRANSFORMTb4b4b4)
Te6edf3cipherTb4b4b4.Te6edf3initTb4b4b4(Te6edf3CipherTb4b4b4.Te6edf3DECRYPT_MODETb4b4b4, Te6edf3keyTb4b4b4, Te6edf3GCMParameterSpecTb4b4b4(Te6edf3GCM_TAG_BITSTb4b4b4, Te6edf3ivTb4b4b4)Tb4b4b4)
Tff7b72return Te6edf3cipherTb4b4b4.Te6edf3doFinalTb4b4b4(Te6edf3ciphertextTb4b4b4)
Tb4b4b4}
T8b949e// endregion
T8b949e// region KeyStore
Tff7b72private Tff7b72fun Td2a8ffloadOrCreateMasterKeyTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3SecretKey Tb4b4b4{
Tff7b72val Te6edf3ksFile Tff7b72= Te6edf3FileTb4b4b4(Te6edf3storeDirTb4b4b4, Te6edf3KEYSTORE_FILETb4b4b4)
Tff7b72val Te6edf3ks Tff7b72= Te6edf3KeyStoreTb4b4b4.Te6edf3getInstanceTb4b4b4(Te6edf3KEYSTORE_TYPETb4b4b4)
Tff7b72val Te6edf3protection Tff7b72= Te6edf3KeyStoreTb4b4b4.Te6edf3PasswordProtectionTb4b4b4(Te6edf3KEYSTORE_PASSWORDTb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3ksFileTb4b4b4.Te6edf3existsTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3FileInputStreamTb4b4b4(Te6edf3ksFileTb4b4b4)Tb4b4b4.Te6edf3use Tb4b4b4{ Te6edf3ksTb4b4b4.Te6edf3loadTb4b4b4(Tffa657itTb4b4b4, Te6edf3KEYSTORE_PASSWORDTb4b4b4) Tb4b4b4}
Tff7b72val Te6edf3entry Tff7b72= Te6edf3ksTb4b4b4.Te6edf3getEntryTb4b4b4(Te6edf3KEY_ALIASTb4b4b4, Te6edf3protectionTb4b4b4)
T8b949e// Fail loudly rather than regenerate: overwriting the master key would orphan every existing .enc backup.
Te6edf3checkTb4b4b4(Te6edf3entry Tff7b72is Te6edf3KeyStoreTb4b4b4.Te6edf3SecretKeyEntryTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffKeystore exists but master key Tffd700$Te6edf3KEY_ALIASTa5d6ff is missing/invalidTa5d6ff" Tb4b4b4}
Tff7b72return Te6edf3entryTb4b4b4.Te6edf3secretKey
Tb4b4b4}
Tff7b72val Te6edf3keyGen Tff7b72= Te6edf3KeyGeneratorTb4b4b4.Te6edf3getInstanceTb4b4b4(Te6edf3AES_ALGORITHMTb4b4b4)
Te6edf3keyGenTb4b4b4.Te6edf3initTb4b4b4(Te6edf3AES_KEY_BITSTb4b4b4)
Tff7b72val Te6edf3secretKey Tff7b72= Te6edf3keyGenTb4b4b4.Te6edf3generateKeyTb4b4b4(Tb4b4b4)
Te6edf3ksTb4b4b4.Te6edf3loadTb4b4b4(Tff7b72nullTb4b4b4, Te6edf3KEYSTORE_PASSWORDTb4b4b4)
Te6edf3ksTb4b4b4.Te6edf3setEntryTb4b4b4(Te6edf3KEY_ALIASTb4b4b4, Te6edf3KeyStoreTb4b4b4.Te6edf3SecretKeyEntryTb4b4b4(Te6edf3secretKeyTb4b4b4)Tb4b4b4, Te6edf3protectionTb4b4b4)
Te6edf3FileOutputStreamTb4b4b4(Te6edf3ksFileTb4b4b4)Tb4b4b4.Te6edf3use Tb4b4b4{ Te6edf3ksTb4b4b4.Te6edf3storeTb4b4b4(Tffa657itTb4b4b4, Te6edf3KEYSTORE_PASSWORDTb4b4b4) Tb4b4b4}
Tff7b72return Te6edf3secretKey
Tb4b4b4}
T8b949e// endregion
Tff7b72private Tff7b72companion Tff7b72object Tb4b4b4{
Tff7b72private Tff7b72const Tff7b72val Te6edf3STORE_DIR Tff7b72= Ta5d6ff"Ta5d6ffsecurity_keysTa5d6ff"
Tff7b72private Tff7b72const Tff7b72val Te6edf3KEYSTORE_FILE Tff7b72= Ta5d6ff"Ta5d6ffkeystore.p12Ta5d6ff"
Tff7b72private Tff7b72const Tff7b72val Te6edf3KEYSTORE_TYPE Tff7b72= Ta5d6ff"Ta5d6ffPKCS12Ta5d6ff"
Tff7b72private Tff7b72const Tff7b72val Te6edf3KEY_ALIAS Tff7b72= Ta5d6ff"Ta5d6ffsecurity_key_backup_masterTa5d6ff"
T8b949e// Intentional: mirrors LockdownPassphraseStoreImpl's documented desktop threat model.
Tff7b72private Tff7b72val Te6edf3KEYSTORE_PASSWORD Tff7b72= Ta5d6ff"Ta5d6ffmeshtastic-security-keysTa5d6ff"Tb4b4b4.Te6edf3toCharArrayTb4b4b4(Tb4b4b4)
Tff7b72private Tff7b72const Tff7b72val Te6edf3AES_ALGORITHM Tff7b72= Ta5d6ff"Ta5d6ffAESTa5d6ff"
Tff7b72private Tff7b72const Tff7b72val Te6edf3AES_GCM_TRANSFORM Tff7b72= Ta5d6ff"Ta5d6ffAES/GCM/NoPaddingTa5d6ff"
Tff7b72private Tff7b72const Tff7b72val Te6edf3AES_KEY_BITS Tff7b72= T79c0ff2T79c0ff5T79c0ff6
Tff7b72private Tff7b72const Tff7b72val Te6edf3GCM_TAG_BITS Tff7b72= T79c0ff1T79c0ff2T79c0ff8
Tff7b72private Tff7b72const Tff7b72val Te6edf3BYTE_MASK Tff7b72= T79c0ff0Te6edf3xFF
Tff7b72private Tff7b72const Tff7b72val Te6edf3SERIALIZED_LINE_COUNT Tff7b72= T79c0ff3
Tb4b4b4}
Tb4b4b4}
Served by rngit 1.5.0 - Generated in 0.06s